#pragma once
#include <glad/glad.h>
#include "IGLProcLoader.h"
#include "Config.h"
#include "core/CImage.h"

struct FBO {
	GLuint colorBuf = 0;
	GLuint texture = 0;
	GLuint depthBuf = 0;
	GLenum attachment;
	UInt texUnit;
	CString sampler2DName;

	FBO(UInt texUnit = 0, const CString& sampler2DName = "", GLenum attachment = GL_COLOR_ATTACHMENT0) {
		this->texUnit = texUnit;
		this->sampler2DName = sampler2DName;
		this->attachment = attachment;
	}

	~FBO() {
		if (texture != 0) glDeleteTextures(1, &texture);
		if (colorBuf != 0) glDeleteFramebuffers(1, &colorBuf);
		if (depthBuf != 0) glDeleteRenderbuffers(1, &depthBuf);
	}
};

/** 
 * @note State Machine: If you want to switch state on/off, you must call it manually even if the code block ends, otherwise, the state continues.
 * @note Bind(Link...etc.): When a attribute array object or buffer object is bound to a target, the previous binding for that target is automatically broken.
 */
class GLState {
public:
	static void init(IGLProcLoader* procLoader) {
		LogI << "GLState::init......";
		static auto ProcLoader = procLoader;
		auto loadproc = [](const char* name) -> void* { return ProcLoader->getProcAddress(name); };
		bool loadSuccess = IsEnv_Linux ? gladLoadGLES2Loader(loadproc) : gladLoadGLLoader(loadproc);
		ASSERT(loadSuccess, "glad init fail!!!!!!");
		ASSERT(glClear != nullptr, "gl function pointer not loaded correctly!!!!!!");

		setViewport(W3D, H3D);
		/** @note clear buffered colored pixels with a new color to fill */
		const CColor& color = Config::Render_ClearColor;
		glClearColor(color.redF(), color.greenF(), color.blueF(), color.alphaF());// can be transparent
		glClearDepthf(1.0);// default
		glFrontFace(GL_CCW);// default

		/** @link https://registry.khronos.org/OpenGL-Refpages/es3/html/glPixelStorei.xhtml */
		glPixelStorei(GL_UNPACK_ALIGNMENT, 1);CHECK_GL_ERR // default:ALIGNMENT=4(you must ensure all size be 4-times)

		enableCullFace();
		enableDepthTest();

		checkIfMultisamplingEnabled();
		checkMaxTextureUnitsSupported();
	}

	/**
	* @brief render on the whole framebuffer, complete from the lower left corner to the upper right
	* @note viewport determine the FBO size
	*/
	static void setViewport(int w, int h, int x = 0, int y = 0) { glViewport(x, y, w, h); }

	/** @note embed device may not support multisampling to do Anti-Aliasing*/
	static void checkIfMultisamplingEnabled() {
		GLint maxSamples = -1, sampleBuffers = -1, sampleCount = -1;
		// samples should be set by the window-glContext/surface first
		glGetIntegerv(GL_SAMPLE_BUFFERS, &sampleBuffers);
		glGetIntegerv(GL_SAMPLES, &sampleCount);
		glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);

		auto info = CString("Has enabled sample buffers(at least one): %1, Has enabled sample count(at least two): %2; Sys-supported maxSamples: %3").arg(sampleBuffers).arg(sampleCount).arg(maxSamples);
		if (maxSamples <= 1 || sampleBuffers <= 0 || sampleCount <= 1)
			LogW << "Multi-Sampling may not-enabled by surface or GL_MULTISAMPLE not supported! => " << info;
		else
			LogI << "GL_MULTISAMPLE is supported and enabled! => " << info;
	}

	static void checkMaxTextureUnitsSupported() {
		GLint max_fragment_units;
		glGetIntegerv(GL_MAX_TEXTURE_IMAGE_UNITS, &max_fragment_units);
		LogI << "support maximum TEXTURE_IMAGE_UNITS is: " << max_fragment_units;
	}

	/**
	* @note two frame buffers: a front buffer and a back buffer
	* @param attachment must be GL_COLOR_ATTACHMENTi, GL_DEPTH_ATTACHMENT, GL_STENCIL_ATTACHMENT or GL_DEPTH_STENCIL_ATTACHMENT.
	*/
	static FBO* createFBO(float ssaa = SSAA, UInt texUnit = 0, const CString& sampler2DName = "", GLenum attachment = GL_COLOR_ATTACHMENT0) {
		const int W3D_SSAA = W3D * ssaa;
		const int H3D_SSAA = H3D * ssaa;
		auto fbo = new FBO(texUnit, sampler2DName, attachment);
		glGenTextures(1, &fbo->texture);
		glActiveTexture(GL_TEXTURE0 + texUnit);CHECK_GL_ERR
		glBindTexture(GL_TEXTURE_2D, fbo->texture);CHECK_GL_ERR
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, W3D_SSAA, H3D_SSAA, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0);CHECK_GL_ERR// multiSamples = 0
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_MODE, GL_COMPARE_REF_TO_TEXTURE);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_COMPARE_FUNC, GL_LEQUAL);

		glGenFramebuffers(1, &fbo->colorBuf);
		glBindFramebuffer(GL_FRAMEBUFFER, fbo->colorBuf);CHECK_GL_ERR
		glFramebufferTexture2D(GL_FRAMEBUFFER, fbo->attachment, GL_TEXTURE_2D, fbo->texture, 0);CHECK_GL_ERR
		// Always check that our framebuffer is ok
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			EXIT("FrameBuffer is not complete!")
		/**
		* @note RenderBuffer differ from FrameBuffer in terms of functionality (RBO: Render Buffer Object)
		* create depth buffer, otherwise, no 'hidden surface removal'
		*/
		glGenRenderbuffers(1, &fbo->depthBuf);
		glBindRenderbuffer(GL_RENDERBUFFER, fbo->depthBuf);CHECK_GL_ERR
		GLenum internalformat = GL_DEPTH_COMPONENT24;
		if (!IsEnv_Linux) internalformat = GL_DEPTH_COMPONENT32;
		glRenderbufferStorage(GL_RENDERBUFFER, internalformat, W3D_SSAA, H3D_SSAA);CHECK_GL_ERR// multiSamples = 0
		// attach a renderbuffer as a logical buffer to the currently bound framebuffer object
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, fbo->depthBuf);CHECK_GL_ERR
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			EXIT("RenderBuffer is not complete!");
		return fbo;
	}

	static void deleteGLTexture(GLuint texture) { glDeleteTextures(1, &texture); }

	static GLuint createMultisampledFBO(GLuint& msColorBuffer, GLuint& msDepthBuffer, int samples) {
		GLuint msFBO;
		glGenFramebuffers(1, &msFBO);
		glBindFramebuffer(GL_FRAMEBUFFER, msFBO);

		glGenRenderbuffers(1, &msColorBuffer);
		glBindRenderbuffer(GL_RENDERBUFFER, msColorBuffer);
		glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, GL_RGBA8, W3D, H3D);// 4x MSAA
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, msColorBuffer);
		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			EXIT("multisample renderBuffer(COLOR_ATTACHMENT0) is not complete!");

		glGenRenderbuffers(1, &msDepthBuffer);
		glBindRenderbuffer(GL_RENDERBUFFER, msDepthBuffer);
		GLenum internalformat = GL_DEPTH_COMPONENT24;
		if (!IsEnv_Linux) internalformat = GL_DEPTH_COMPONENT32;
		glRenderbufferStorageMultisample(GL_RENDERBUFFER, samples, internalformat, W3D, H3D);
		glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, msDepthBuffer);

		if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
			EXIT("multisample renderBuffer(DEPTH_ATTACHMENT) is not complete!");

		glBindFramebuffer(GL_FRAMEBUFFER, 0);
		return msFBO;
		// Later Step:
		// parse the multi sampled frame buffer into a regular frame buffer
		//glBindFramebuffer(GL_READ_FRAMEBUFFER, msFBO);
		//glBindFramebuffer(GL_DRAW_FRAMEBUFFER, resolvedFBO);
		//glBlitFramebuffer(0, 0, WIDTH, HEIGHT, 0, 0, WIDTH, HEIGHT, GL_COLOR_BUFFER_BIT, GL_NEAREST);
	}

	static void bind(FBO* fbo, float ssaa = SSAA) {
		if (!fbo) {
			bindDefault();
			return;
		}
		const int W3D_SSAA = W3D * ssaa;
		const int H3D_SSAA = H3D * ssaa;
		setViewport(W3D_SSAA, H3D_SSAA);
		glBindFramebuffer(GL_FRAMEBUFFER, fbo->colorBuf);
		glBindRenderbuffer(GL_RENDERBUFFER, fbo->depthBuf);
		/** @note the fragment shader output value is written into the attachments color buffer. */
		GLenum drawBuffers[1] = {fbo->attachment};
		glDrawBuffers(1, drawBuffers);
	}

	//static void bindRBO(GLuint rbo) { glBindRenderbuffer(GL_RENDERBUFFER, rbo); }

	static void bindDefault() {// bind default fbo (which is the `screen buffer` ---- display directly)
		setViewport(W3D, H3D);
		glBindFramebuffer(GL_FRAMEBUFFER, 0);// reset to default
		glBindRenderbuffer(GL_RENDERBUFFER, 0);// reset to default
	}

	/**
	* @note OpenGL will use GL_TEXTURE0 as the current active texture unit (so can ignore glActiveTexture(GL_TEXTURE0)).
	*       If you do not explicitly activate other texture units, all texture operations will be applied to GL_TEXTURE0 by default.
	*	    so, no uniform name needed if visit the current active texture unit, you can use any name to accept the sampler2D texture data.
	*/
	static void bindTexture(GLuint texture, UInt texUnit = 0) {// corresponding to GLSL type: Sampler2D with the default GL_TEXTURE0
		glActiveTexture(GL_TEXTURE0 + texUnit);
		glBindTexture(GL_TEXTURE_2D, texture);
	}

	static void captureFBO(const CString& saveImgPath = "", bool hasAlphaChannel = true, int x = InvalidNum, int y = InvalidNum, int w = 0, int h = 0, float ssaa = SSAA) {
		const int W3D_SSAA = W3D * ssaa;
		const int H3D_SSAA = H3D * ssaa;
		void* data = new GLubyte[W3D_SSAA * H3D_SSAA * 4];// Assuming 4 bytes per pixel for GL_RGBA, don't forget to delete!!!
		glReadBuffer(GL_COLOR_ATTACHMENT0);CHECK_GL_ERR
		glReadPixels(0, 0, W3D_SSAA, H3D_SSAA, GL_RGBA, GL_UNSIGNED_BYTE, data);CHECK_GL_ERR
		if (!saveImgPath.empty())
			saveImageByData(data, saveImgPath, hasAlphaChannel, x, y, w, h, ssaa);
	}

	/** @note draw opaque disable writing into depth buffer, but enable depth test.
	*   drawOpaque -> disable writing into depth buf -> drawTransparent -> enable writing into depth buf.
	*/
	static void enableDepthWrite() { glDepthMask(GL_TRUE); }
	static void disableDepthWrite() { glDepthMask(GL_FALSE); }

	/**
	 * @note force execution of GL commands in finite time(Not waiting all the time), call it when all model(including sub models) completed drawing.
	 * we need to flush the contents to the FBO before posting the texture to the other thread, otherwise, we might get unexpected results.
	 */
	static void flush() {
		glFlush();// flush the contents to the FBO, not waiting all the time until finished
	}

	/**
	* @brief glFinish does not return until the effects of all previously called GL commands are complete.
	* Such effects include all changes to GL state, all changes to connection state, and all changes to the frame buffer contents.
	*/
	static void waitToFinish() {
		glFinish();// block until all GL execution is complete
	}

	/** 
	* @param framebuffer clear the bound frameBuffer
	* @param mask: Bitwise OR of masks that indicate the buffers to be cleared.
	*/
	static void clearBoundBuffer(GLbitfield mask = GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT) {
		glClear(mask);
	}

	static void bindAndClear(FBO* fbo, float ssaa = SSAA) {
		bind(fbo, ssaa);
		clearBoundBuffer();
	}

	/**@brief specify the clear value for the stencil buffer */
	static void clearStencilBuffer(GLint s = 0) {
		glClearStencil(s);
		glClear(GL_STENCIL_BUFFER_BIT);
	}

	static void enableDepthTest(GLenum func = GL_LEQUAL) {// default: disabled
		glEnable(GL_DEPTH_TEST);
		glDepthFunc(func);
	}
	static void disableDepthTest() { glDisable(GL_DEPTH_TEST); }

	static void enableCullFace(GLenum mode = GL_BACK) {// default: disabled
		glEnable(GL_CULL_FACE);
		glCullFace(mode);
	}
	static void disableCullFace() { glDisable(GL_CULL_FACE); }

	static void enableBlend(GLenum sourceRgba = GL_SRC_ALPHA, GLenum destRgba = GL_ONE_MINUS_SRC_ALPHA, GLenum mode = GL_FUNC_ADD) {// default: disabled
		glEnable(GL_BLEND);
		glBlendFunc(sourceRgba, destRgba);
		glBlendEquation(mode);
	}
	static void disableBlend() { glDisable(GL_BLEND); }

	/**
	 * @brief Steps:
	 * 0. Reset, always pass stencil test, just do as default;
	 * \example (0)=> glStencilMask(0xFF); glStencilFunc(GL_ALWAYS, 0, 0xFF);
	 * 1. Draw Normal-Objects, but don't write the Normal-Objects to the stencil buffer,
		  we only care about the CreatingStencilBuf-Objects,
		  so we set its mask to 0x00 to not write to the stencil buffer, then draw Normal-Objects;
	 * \example (1)=> glStencilMask(0x00); glDrawNormal-Objects;
	 * 2. Draw CreatingStencilBuf-Objects, so we set its mask to 0xFF writing to the stencil buffer;
	 * \example (2)=> glStencilMask(0xFF); glStencilFunc(GL_ALWAYS, 1, 0xFF); glDrawCreatingStencilBuf-Objects;
	 * 3. Draw UsingStencilBuf-Objects, disabling stencil writing, because the stencil buffer is now filled with several 1s,
		  the parts of the stencil buffer that are 1 is the pixel area of the CreatingStencilBuf-Objects;
	 * \example If want to render only the stencilValue = 1 area: func = GL_EQUAL otherwise func = GL_NOTEQUAL, etc.
	 * \example (3)=> glStencilMask(0x00); glStencilFunc(func, 1, 0xFF); glDrawUsingStencilBuf-Objects
	 */
	static void resetStencilTest() { glStencilMask(0xFF); glStencilFunc(GL_ALWAYS, 0, 0xFF); }// the RenderRecycle begins
	static void disableStencilWrite() { glStencilMask(0x00); }// => glDrawNormal-Objects
	static void genStencilBuffer() { glStencilMask(0xFF); glStencilFunc(GL_ALWAYS, 1, 0xFF); }// => glDrawCreatingStencilBuf-Objects
	static void useStencilBuffer() { glStencilMask(0x00); glStencilFunc(GL_NOTEQUAL, 1, 0xFF); }// => glDrawUsingStencilBuf-Objects
	/** 
	 * @brief Pipline: 
	 * output of Fragment Shader
			|
			v
		Stencil Test
			|
			|-- pass --> Depth Test --> write into color buffer
			|
			|-- not pass --> discard fragment
	 *  vertex shader -> {primitive assembly} -> {rasterization} -> fragment shader
	 *  -> stencil test -> depth test -> blending -> [dithering] =>write to FBO
	 * @param dppass = GL_REPLACE: sets the stencil buffer value to `ref`, as specified by glStencilFunc(func, `ref`, mask).
	 */
	static void enableStencilTest(GLenum sfail = GL_KEEP, GLenum dpfail = GL_KEEP, GLenum dppass = GL_REPLACE) {
		glEnable(GL_STENCIL_TEST);
		glStencilOp(sfail, dpfail, dppass);CHECK_GL_ERR// set front and back stencil test actions
	}
	static void disableStencilTest() { glDisable(GL_STENCIL_TEST); }

	static void setStencilFunc(GLenum func, GLint ref, GLuint mask = 0xFF) {
		glStencilFunc(func, ref, mask);CHECK_GL_ERR// args: GLenum func, GLint ref, GLuint mask
	}

	static void bindBgTexture(bool isDarkMode) {
		if (isDarkMode) {
			if (s_darkBg == 0)
				genBgTexture(true);
			bindTexture(s_darkBg);
		} else {
			if (s_lightBg == 0)
				genBgTexture(false);
			bindTexture(s_lightBg);
		}
	}

	static void genBgTexture(bool isDarkMode, GLuint texUnit = 0);

	static void releaseBgTextures() {
		if (s_darkBg != 0) {
			glDeleteTextures(1, &s_darkBg);
			s_darkBg = 0;
		}
		if (s_lightBg != 0) {
			glDeleteTextures(1, &s_lightBg);
			s_lightBg = 0;
		}
	}

	static GLuint loadTexture(const CImage& textureImg, GLuint texUnit = 0);

	static void saveImageByData(void* pixelData, const std::string& savePath, bool hasAlphaChannel = true, int cropX = InvalidNum, int cropY = InvalidNum, int cropW = 0, int cropH = 0, float ssaa = SSAA);

private:
	GLState() = default;

	static GLuint s_darkBg, s_lightBg;
};